今天來討論比較進階的效能問題,我們俗稱的「long list」,也就是畫面上那種很長的 list 或 table,滾輪要一直滑才會到最底的那種。
程式方面要寫出來其實不難,因為很單純就是
.map()
產生 JSX 語法 HTML所以不管資料是幾十筆、幾百筆,反正程式都一樣,都是跑 .map()
,只是產出來之後才會發現,雖然畫面內容都正確,卻會有失幀、lag 等效能 issue。
到了這時才會發現,寫對是一回事,寫好更是需要下功夫。
在面對 long list 的時候,其實一開始是沒什麼感覺的,畢竟剛開始沒什麼資料,畫面一下就 render 好了,就算改個 state 重新 render,也是一轉眼之間的事情。
但隨著使用者增加,資料漸增,效能方面也開始受到影響,不再是一轉眼的事情。
例如 FB、Twitter 等社群媒體會有大量的文章列表,如果有 1000 篇文章,就必須生成 1000 個 DOM 節點,每個節點底下還包含了圖片、影音等多媒體資料。
像這樣同時 render 數量龐大的元素會有幾個明顯的缺點:
React 官方建議要渲染 long list 時,可以使用 Virtualized list 技術來最佳化效能,是優化 long list 的一種技巧:
儲存所有列表元素的位置,只渲染可視區(viewport)內的列表元素,當可視區滾動時,根據滾動的 offset 大小以及所有列表元素的位置,計算在可視區應該渲染哪些元素,這種技術也叫做「Windowing」
正常的 element 排列方式是 static
,每個 element 會按照順序「佔位子」,因此每個 element 是互相影響關聯的(上面的元素長愈高,下面的元素就愈被往下擠)
但為了做到 windowing 的效果,「不是所有 element 都需要 render 在畫面上」,因此需要打破這個關聯,改用 absolute
來排列,需透過每個 element 的高度,幫每個 element 直接指定一個絕對位置,這樣就可以明確知道,當我滑動到某個距離時,該 render 哪些 element
需要透過以下資料,來做出 windowing 的效果:
已知
可以算出每個 item 相對於 Long List 的絕對距離,再根據目前的滑動距離計算出,目前的 window 要顯示的 item 範圍,就可以只 render window 內的 item。
使用者滾動之後,上述流程再跑一次。
Long List
Virtualized List
Infinite scrolling
其中官方建議的兩個套件 react-window 跟 react-virtualized,作者是同一個人(Brian Vaughn)。
兩者的區別只在於, 前者是後者的優化版,更快更輕量,基本上都用優化版即可,但因為 react-window 是針對常用的 element 架構(list & grid),因此若有比較特別的架構,還是要回歸到 react-virtualized
更詳細的差異可參考 [React-window 作者本人的說法](https://github.com/bvaughn/react-window#how-is-react-window-different-from-react-virtualized
Infinite scrolling 可以參考 react-window-infinite-loader
大幅減少 render 時間,解決失幀問題,提升 UX
針對資料量龐大,但無法切不同頁面處理時,可以用來處理 HTML 格式一致、高度盡量單純的 case,尤其需要把 RWD 的狀況考慮進來。
還有許多情境可以參考 React-window 作者簡報
在公司中使用 React-window 時,其實踩了滿多坑的,剛開始套用時覺得很神奇,真的是大幅提升了 render 的速度。
但最大的坑在於,一定要確保子項目的高度在控制範圍內,比如說在 PC 版本每個子項目都寫死 60px
高度,但切換 RWD 到 mobile 版本時,可能文字太多就不小心換行,高度就被撐開了。
Virtualized List 因為是靠 absolute
定位高度,因此一定要能夠確保在任何情況下,掌控子項目高度不會暴走,否則整個 list 就會跑版很可怕。
使用 react-window 虚拟化大型列表
今晚,我想來點 Web 前端效能優化大補帖!